from dataclasses import fields, is_dataclass
from typing import Union


class NotSet:
    pass


def origin_type(T):
    return getattr(T, "__origin__", None)


def type_args(T):
    return getattr(T, "__args__", [])


def is_optional(T):
    return origin_type(T) is Union and type_args(T) and type_args(T)[-1] is type(None)


def unpack_optional(T):
    return type_args(T)[0]


def restore_from_dict(cls, data):
    """
    :param cls: A class, subtype of type dataclass
    :param data: dict with data to apply to the class when instantiated

    Note: This function won't work if you `from __future__ import annotations`
    """
    if is_dataclass(data) or not is_dataclass(cls):
        return data

    ff = fields(cls)
    tx_data = {}
    for f in ff:
        name = f.name
        T = f.type

        if f.init is False:
            data.pop(name, None)
            continue

        is_opt = is_optional(T)
        if is_opt:
            T = unpack_optional(T)

        if is_opt:
            v = data.get(name, NotSet)
        else:
            # Intended to throw Key exception.
            # If you catch, repackage into another exception.
            v = data[name]

        if v is NotSet:
            continue  # for loop

        if is_dataclass(T):
            tx_data[name] = restore_from_dict(T, v)
            continue  # for loop

        if origin_type(T) is list:
            T = type_args(T)[0]
            tx_data[name] = [restore_from_dict(T, e) for e in v]
            continue  # for loop

        if origin_type(T) is dict:
            _, T = type_args(T)
            tx_data[name] = {k: restore_from_dict(T, e) for k, e in v.items()}
            continue

        # it's not a dataclass or a simple struct of them.
        # must some literal value
        # or some incomprehensible type that we don't yet care to parse, not our problem
        tx_data[name] = v

    return cls(**tx_data)
